@abraca/cli 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/abracadabra-cli.cjs +78 -50
- package/dist/abracadabra-cli.cjs.map +1 -1
- package/dist/abracadabra-cli.esm.js +78 -50
- package/dist/abracadabra-cli.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/commands/content.ts +5 -3
- package/src/commands/documents.ts +3 -3
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @abraca/cli
|
|
2
|
+
|
|
3
|
+
The `abracadabra` command-line client — read and mutate a space's tree, content, metadata, permissions, files, and presence from a terminal, plus a streaming Wikipedia importer.
|
|
4
|
+
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
Full, code-derived documentation lives in [`docs/`](docs/) — parsing/dispatch,
|
|
8
|
+
connection & auth, the full command reference (read vs mutate), and the Wikipedia
|
|
9
|
+
importer. It is the source of truth.
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ABRA_URL=https://my-server.example.com abracadabra tree
|
|
15
|
+
abracadabra read name="My Page"
|
|
16
|
+
abracadabra write name="My Page" content="# Hello" mode=replace
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`abracadabra [--flag] <command>[:<sub>] [key=value ...]`. Identity is an Ed25519 key file (`ABRA_KEY_FILE`, default `~/.abracadabra/cli.key`); auto-registers on first run. Docs are addressed via `id=`/`name=`/`path=`/positional. Output `--format=json|tsv|tree|text|md`.
|
|
20
|
+
|
|
21
|
+
::
|
|
22
|
+
|
|
23
|
+
> `abracadabra version` reports a hardcoded `1.0.0` (≠ package `2.4.0`). `delete` is a reversible soft-delete; `duplicate` is shallow; `chat` is a stateless `messages:send`. See [`docs/2.commands/reference`](docs/2.commands/1.reference.md).
|
|
24
|
+
|
|
25
|
+
## License
|
|
26
|
+
|
|
27
|
+
MIT.
|
package/dist/abracadabra-cli.cjs
CHANGED
|
@@ -1440,6 +1440,23 @@ function parseFrontmatter(markdown) {
|
|
|
1440
1440
|
body
|
|
1441
1441
|
};
|
|
1442
1442
|
}
|
|
1443
|
+
function pushNested(out, inner, wrap) {
|
|
1444
|
+
const children = parseInline(inner);
|
|
1445
|
+
if (children.length === 0) {
|
|
1446
|
+
out.push({
|
|
1447
|
+
text: inner,
|
|
1448
|
+
attrs: { ...wrap }
|
|
1449
|
+
});
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
for (const child of children) out.push({
|
|
1453
|
+
text: child.text,
|
|
1454
|
+
attrs: {
|
|
1455
|
+
...child.attrs ?? {},
|
|
1456
|
+
...wrap
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1443
1460
|
function parseInline(text) {
|
|
1444
1461
|
const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
|
|
1445
1462
|
const tokens = [];
|
|
@@ -1488,22 +1505,10 @@ function parseInline(text) {
|
|
|
1488
1505
|
text: label,
|
|
1489
1506
|
attrs: { docLink: { docId } }
|
|
1490
1507
|
});
|
|
1491
|
-
} else if (match[10] !== void 0) tokens
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
});
|
|
1495
|
-
else if (match[11] !== void 0) tokens.push({
|
|
1496
|
-
text: match[11],
|
|
1497
|
-
attrs: { bold: true }
|
|
1498
|
-
});
|
|
1499
|
-
else if (match[12] !== void 0) tokens.push({
|
|
1500
|
-
text: match[12],
|
|
1501
|
-
attrs: { italic: true }
|
|
1502
|
-
});
|
|
1503
|
-
else if (match[13] !== void 0) tokens.push({
|
|
1504
|
-
text: match[13],
|
|
1505
|
-
attrs: { italic: true }
|
|
1506
|
-
});
|
|
1508
|
+
} else if (match[10] !== void 0) pushNested(tokens, match[10], { strike: true });
|
|
1509
|
+
else if (match[11] !== void 0) pushNested(tokens, match[11], { bold: true });
|
|
1510
|
+
else if (match[12] !== void 0) pushNested(tokens, match[12], { italic: true });
|
|
1511
|
+
else if (match[13] !== void 0) pushNested(tokens, match[13], { italic: true });
|
|
1507
1512
|
else if (match[14] !== void 0) tokens.push({
|
|
1508
1513
|
text: match[14],
|
|
1509
1514
|
attrs: { code: true }
|
|
@@ -2039,11 +2044,19 @@ function parseBlocks(markdown) {
|
|
|
2039
2044
|
function fillTextInto(el, tokens) {
|
|
2040
2045
|
const filtered = tokens.filter((t) => t.text.length > 0);
|
|
2041
2046
|
if (!filtered.length) return;
|
|
2042
|
-
const
|
|
2043
|
-
|
|
2047
|
+
const children = filtered.map((tok) => {
|
|
2048
|
+
return (tok.attrs?.docLink)?.docId ? new yjs.XmlElement("docLink") : new yjs.XmlText();
|
|
2049
|
+
});
|
|
2050
|
+
el.insert(0, children);
|
|
2044
2051
|
filtered.forEach((tok, i) => {
|
|
2045
|
-
|
|
2046
|
-
|
|
2052
|
+
const node = children[i];
|
|
2053
|
+
if (node instanceof yjs.XmlElement) {
|
|
2054
|
+
const dl = tok.attrs.docLink;
|
|
2055
|
+
node.setAttribute("docId", dl.docId);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (tok.attrs) node.insert(0, tok.text, tok.attrs);
|
|
2059
|
+
else node.insert(0, tok.text);
|
|
2047
2060
|
});
|
|
2048
2061
|
}
|
|
2049
2062
|
function blockElName(b) {
|
|
@@ -2399,6 +2412,15 @@ function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled"
|
|
|
2399
2412
|
|
|
2400
2413
|
//#endregion
|
|
2401
2414
|
//#region packages/convert/src/yjs-to-markdown.ts
|
|
2415
|
+
function isXElem(n) {
|
|
2416
|
+
return !!n && typeof n.nodeName === "string";
|
|
2417
|
+
}
|
|
2418
|
+
function isXText(n) {
|
|
2419
|
+
return !!n && typeof n.nodeName !== "string" && typeof n.toDelta === "function";
|
|
2420
|
+
}
|
|
2421
|
+
function localizeFragment(fragment) {
|
|
2422
|
+
return fragment;
|
|
2423
|
+
}
|
|
2402
2424
|
function serializeDelta(delta) {
|
|
2403
2425
|
let result = "";
|
|
2404
2426
|
for (const op of delta) {
|
|
@@ -2459,12 +2481,15 @@ function serializeDelta(delta) {
|
|
|
2459
2481
|
}
|
|
2460
2482
|
function serializeInline(el) {
|
|
2461
2483
|
const parts = [];
|
|
2462
|
-
for (const child of el.toArray()) if (child
|
|
2463
|
-
else if (child
|
|
2484
|
+
for (const child of el.toArray()) if (isXText(child)) parts.push(serializeDelta(child.toDelta()));
|
|
2485
|
+
else if (isXElem(child)) if (child.nodeName === "docLink") {
|
|
2486
|
+
const docId = child.getAttribute("docId") ?? "";
|
|
2487
|
+
parts.push(`[[${docId}]]`);
|
|
2488
|
+
} else parts.push(serializeInline(child));
|
|
2464
2489
|
return parts.join("");
|
|
2465
2490
|
}
|
|
2466
2491
|
function serializeBlock(el, indent = "") {
|
|
2467
|
-
if (el
|
|
2492
|
+
if (isXText(el)) return serializeDelta(el.toDelta());
|
|
2468
2493
|
switch (el.nodeName) {
|
|
2469
2494
|
case "documentHeader":
|
|
2470
2495
|
case "documentMeta": return "";
|
|
@@ -2484,7 +2509,7 @@ function serializeBlock(el, indent = "") {
|
|
|
2484
2509
|
}
|
|
2485
2510
|
case "blockquote": {
|
|
2486
2511
|
const lines = [];
|
|
2487
|
-
for (const child of el.toArray()) if (child
|
|
2512
|
+
for (const child of el.toArray()) if (isXElem(child)) {
|
|
2488
2513
|
const text = serializeBlock(child);
|
|
2489
2514
|
for (const line of text.split("\n")) lines.push(`> ${line}`);
|
|
2490
2515
|
}
|
|
@@ -2545,11 +2570,11 @@ function serializeBlock(el, indent = "") {
|
|
|
2545
2570
|
if (to) props.push(`to="${to}"`);
|
|
2546
2571
|
return `::card${props.length ? `{${props.join(" ")}}` : ""}\n${serializeChildren(el)}\n::`;
|
|
2547
2572
|
}
|
|
2548
|
-
case "cardGroup": return `::card-group\n${el.toArray().filter((c) => c
|
|
2549
|
-
case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => c
|
|
2550
|
-
case "codeGroup": return `::code-group\n${el.toArray().filter((c) => c
|
|
2573
|
+
case "cardGroup": return `::card-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2574
|
+
case "codeCollapse": return `::code-collapse\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2575
|
+
case "codeGroup": return `::code-group\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2551
2576
|
case "codePreview": {
|
|
2552
|
-
const children = el.toArray().filter((c) => c
|
|
2577
|
+
const children = el.toArray().filter((c) => isXElem(c));
|
|
2553
2578
|
const nonCode = children.filter((c) => c.nodeName !== "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
|
|
2554
2579
|
const code = children.filter((c) => c.nodeName === "codeBlock").map((c) => serializeBlock(c)).join("\n\n");
|
|
2555
2580
|
const parts = [nonCode];
|
|
@@ -2567,16 +2592,16 @@ function serializeBlock(el, indent = "") {
|
|
|
2567
2592
|
if (required === true || required === "true") props.push("required=\"true\"");
|
|
2568
2593
|
return `::field{${props.join(" ")}}\n${serializeChildren(el)}\n::`;
|
|
2569
2594
|
}
|
|
2570
|
-
case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => c
|
|
2595
|
+
case "fieldGroup": return `::field-group\n${el.toArray().filter((c) => isXElem(c)).map((c) => serializeBlock(c)).join("\n\n")}\n::`;
|
|
2571
2596
|
default: return serializeChildren(el);
|
|
2572
2597
|
}
|
|
2573
2598
|
}
|
|
2574
2599
|
function serializeChildren(el) {
|
|
2575
2600
|
const blocks = [];
|
|
2576
|
-
for (const child of el.toArray()) if (child
|
|
2601
|
+
for (const child of el.toArray()) if (isXElem(child)) {
|
|
2577
2602
|
const text = serializeBlock(child);
|
|
2578
2603
|
if (text) blocks.push(text);
|
|
2579
|
-
} else if (child
|
|
2604
|
+
} else if (isXText(child)) {
|
|
2580
2605
|
const text = serializeDelta(child.toDelta());
|
|
2581
2606
|
if (text) blocks.push(text);
|
|
2582
2607
|
}
|
|
@@ -2586,11 +2611,11 @@ function serializeListItems(el, type, indent) {
|
|
|
2586
2611
|
const lines = [];
|
|
2587
2612
|
let counter = 1;
|
|
2588
2613
|
for (const child of el.toArray()) {
|
|
2589
|
-
if (!(child
|
|
2614
|
+
if (!isXElem(child) || child.nodeName !== "listItem") continue;
|
|
2590
2615
|
const prefix = type === "bullet" ? "- " : `${counter++}. `;
|
|
2591
2616
|
const subParts = [];
|
|
2592
2617
|
for (const sub of child.toArray()) {
|
|
2593
|
-
if (!(sub
|
|
2618
|
+
if (!isXElem(sub)) continue;
|
|
2594
2619
|
if (sub.nodeName === "bulletList") subParts.push(serializeListItems(sub, "bullet", indent + " "));
|
|
2595
2620
|
else if (sub.nodeName === "orderedList") subParts.push(serializeListItems(sub, "ordered", indent + " "));
|
|
2596
2621
|
else subParts.push(serializeInline(sub));
|
|
@@ -2606,13 +2631,13 @@ function serializeListItems(el, type, indent) {
|
|
|
2606
2631
|
function serializeTaskList(el, indent) {
|
|
2607
2632
|
const lines = [];
|
|
2608
2633
|
for (const child of el.toArray()) {
|
|
2609
|
-
if (!(child
|
|
2634
|
+
if (!isXElem(child) || child.nodeName !== "taskItem") continue;
|
|
2610
2635
|
const checked = child.getAttribute("checked");
|
|
2611
2636
|
const marker = checked === true || checked === "true" ? "[x]" : "[ ]";
|
|
2612
2637
|
let header = "";
|
|
2613
2638
|
const nestedParts = [];
|
|
2614
2639
|
for (const sub of child.toArray()) {
|
|
2615
|
-
if (!(sub
|
|
2640
|
+
if (!isXElem(sub)) continue;
|
|
2616
2641
|
if (sub.nodeName === "paragraph" && header === "") header = serializeInline(sub);
|
|
2617
2642
|
else if (sub.nodeName === "bulletList") nestedParts.push(serializeListItems(sub, "bullet", indent + " "));
|
|
2618
2643
|
else if (sub.nodeName === "orderedList") nestedParts.push(serializeListItems(sub, "ordered", indent + " "));
|
|
@@ -2625,16 +2650,16 @@ function serializeTaskList(el, indent) {
|
|
|
2625
2650
|
return lines.join("\n");
|
|
2626
2651
|
}
|
|
2627
2652
|
function getCodeBlockText(el) {
|
|
2628
|
-
for (const child of el.toArray()) if (child
|
|
2653
|
+
for (const child of el.toArray()) if (isXText(child)) return child.toString();
|
|
2629
2654
|
return "";
|
|
2630
2655
|
}
|
|
2631
2656
|
function serializeTable(el) {
|
|
2632
|
-
const rows = el.toArray().filter((c) => c
|
|
2657
|
+
const rows = el.toArray().filter((c) => isXElem(c));
|
|
2633
2658
|
if (!rows.length) return "";
|
|
2634
2659
|
const serializedRows = [];
|
|
2635
2660
|
for (const row of rows) {
|
|
2636
|
-
const cells = row.toArray().filter((c) => c
|
|
2637
|
-
return cell.toArray().filter((c) => c
|
|
2661
|
+
const cells = row.toArray().filter((c) => isXElem(c)).map((cell) => {
|
|
2662
|
+
return cell.toArray().filter((c) => isXElem(c)).map((c) => serializeInline(c)).join(" ");
|
|
2638
2663
|
});
|
|
2639
2664
|
serializedRows.push(cells);
|
|
2640
2665
|
}
|
|
@@ -2653,7 +2678,7 @@ function serializeTable(el) {
|
|
|
2653
2678
|
].join("\n");
|
|
2654
2679
|
}
|
|
2655
2680
|
function serializeSlottedContainer(el, containerName, childName, slotPrefix) {
|
|
2656
|
-
return `::${containerName}\n${el.toArray().filter((c) => c
|
|
2681
|
+
return `::${containerName}\n${el.toArray().filter((c) => isXElem(c) && c.nodeName === childName).map((item) => {
|
|
2657
2682
|
const label = item.getAttribute("label") ?? "";
|
|
2658
2683
|
const icon = item.getAttribute("icon") ?? "";
|
|
2659
2684
|
const props = [];
|
|
@@ -2703,6 +2728,7 @@ function escapeYaml(s) {
|
|
|
2703
2728
|
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
2704
2729
|
}
|
|
2705
2730
|
function yjsToMarkdown(fragment, label, meta, type) {
|
|
2731
|
+
fragment = localizeFragment(fragment);
|
|
2706
2732
|
const { text: headerText, source: titleSource } = readDocumentHeader(fragment);
|
|
2707
2733
|
const effectiveTitle = headerText || label;
|
|
2708
2734
|
const docMeta = readDocumentMeta(fragment);
|
|
@@ -2726,7 +2752,7 @@ function readDocumentMeta(fragment) {
|
|
|
2726
2752
|
const meta = {};
|
|
2727
2753
|
let type;
|
|
2728
2754
|
for (const child of fragment.toArray()) {
|
|
2729
|
-
if (!(child
|
|
2755
|
+
if (!isXElem(child) || child.nodeName !== "documentMeta") continue;
|
|
2730
2756
|
const attrs = child.getAttributes();
|
|
2731
2757
|
for (const k of Object.keys(attrs)) {
|
|
2732
2758
|
const v = attrs[k];
|
|
@@ -2746,8 +2772,8 @@ function readDocumentMeta(fragment) {
|
|
|
2746
2772
|
}
|
|
2747
2773
|
function readDocumentHeader(fragment) {
|
|
2748
2774
|
for (const child of fragment.toArray()) {
|
|
2749
|
-
if (!(child
|
|
2750
|
-
const text = child.toArray().find((c) => c
|
|
2775
|
+
if (!isXElem(child) || child.nodeName !== "documentHeader") continue;
|
|
2776
|
+
const text = child.toArray().find((c) => isXText(c));
|
|
2751
2777
|
const src = child.getAttribute("titleSource");
|
|
2752
2778
|
const source = src === "h1" || src === "frontmatter" ? src : void 0;
|
|
2753
2779
|
return {
|
|
@@ -2760,7 +2786,7 @@ function readDocumentHeader(fragment) {
|
|
|
2760
2786
|
function collectBodyBlocks(fragment) {
|
|
2761
2787
|
const out = [];
|
|
2762
2788
|
for (const child of fragment.toArray()) {
|
|
2763
|
-
if (!(child
|
|
2789
|
+
if (!isXElem(child)) continue;
|
|
2764
2790
|
if (child.nodeName === "documentHeader" || child.nodeName === "documentMeta") continue;
|
|
2765
2791
|
out.push(child);
|
|
2766
2792
|
}
|
|
@@ -3704,17 +3730,17 @@ registerCommand({
|
|
|
3704
3730
|
const docId = resolveDocument(conn, args.params, args.positional);
|
|
3705
3731
|
if (!docId) return "Document not found. Specify id=, name=, or path= to identify the document.";
|
|
3706
3732
|
try {
|
|
3707
|
-
const
|
|
3733
|
+
const markdown = yjsToMarkdown((await conn.getChildProvider(docId)).document.getXmlFragment("default"), "");
|
|
3708
3734
|
if (args.flags.has("json") || args.params["format"] === "json") {
|
|
3709
3735
|
const treeMap = conn.getTreeMap();
|
|
3710
|
-
let label =
|
|
3736
|
+
let label = "";
|
|
3711
3737
|
let type;
|
|
3712
3738
|
let meta;
|
|
3713
3739
|
let children = [];
|
|
3714
3740
|
if (treeMap) {
|
|
3715
3741
|
const entry = treeMap.get(docId);
|
|
3716
3742
|
if (entry) {
|
|
3717
|
-
label = entry.label ||
|
|
3743
|
+
label = entry.label || label;
|
|
3718
3744
|
type = entry.type;
|
|
3719
3745
|
meta = entry.meta;
|
|
3720
3746
|
}
|
|
@@ -4029,7 +4055,7 @@ registerCommand({
|
|
|
4029
4055
|
try {
|
|
4030
4056
|
const doc = (await conn.getChildProvider(docId)).document;
|
|
4031
4057
|
const fragment = doc.getXmlFragment("default");
|
|
4032
|
-
const
|
|
4058
|
+
const existing = yjsToMarkdown(fragment, "");
|
|
4033
4059
|
const text = content.replace(/\\n/g, "\n").replace(/\\t/g, " ");
|
|
4034
4060
|
const combined = args.flags.has("inline") ? text + existing : text + "\n" + existing;
|
|
4035
4061
|
doc.transact(() => {
|
|
@@ -4052,7 +4078,7 @@ registerCommand({
|
|
|
4052
4078
|
const docId = resolveDocument(conn, args.params, args.positional);
|
|
4053
4079
|
if (!docId) return "Document not found.";
|
|
4054
4080
|
try {
|
|
4055
|
-
const
|
|
4081
|
+
const markdown = yjsToMarkdown((await conn.getChildProvider(docId)).document.getXmlFragment("default"), "");
|
|
4056
4082
|
const words = markdown.split(/\s+/).filter(Boolean).length;
|
|
4057
4083
|
const chars = markdown.length;
|
|
4058
4084
|
if (args.flags.has("words")) return String(words);
|
|
@@ -4074,7 +4100,9 @@ registerCommand({
|
|
|
4074
4100
|
const outputPath = args.params["output"] || args.params["to"] || args.params["path"];
|
|
4075
4101
|
if (!outputPath) return "Missing required parameter: output=<filepath>";
|
|
4076
4102
|
try {
|
|
4077
|
-
const
|
|
4103
|
+
const fragment = (await conn.getChildProvider(docId)).document.getXmlFragment("default");
|
|
4104
|
+
const title = (conn.getTreeMap()?.get(docId))?.label;
|
|
4105
|
+
const markdown = yjsToMarkdown(fragment, title ?? "");
|
|
4078
4106
|
const resolvedPath = node_path.resolve(outputPath);
|
|
4079
4107
|
node_fs.writeFileSync(resolvedPath, markdown, "utf-8");
|
|
4080
4108
|
return `Exported "${title || "document"}" to ${resolvedPath} (${Buffer.byteLength(markdown)} bytes)`;
|